package com.ampaiva.pattern.fsm;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

import android.util.Log;

public class FinalStateMachine<O, T, S, E> {
	private static final String TAG = FinalStateMachine.class.getSimpleName();
	private final O owner;

	@Retention(RetentionPolicy.RUNTIME)
	@Target({ ElementType.METHOD })
	public @interface Action {
		String state() default NULL;

		String event() default NULL;

		public static final String NULL = "THIS IS A SPECIAL NULL VALUE - DO NOT USE";
	}

	public FinalStateMachine(O owner) {
		this.owner = owner;
	}

	private Method findAction(S state, E event) {
		Method[] methods = owner.getClass().getMethods();
		for (Method method : methods) {
			Action annotation = method.getAnnotation(Action.class);
			if (!(annotation instanceof Action))
				// null annotation
				continue;

			String stateStr = annotation.state();
			// I don't care about state (state == null), but method does
			// (Action.NULL)
			if (state == null && !stateStr.equals(Action.NULL))
				continue;
			// I do care about state (state != null), but method state is NOT
			// what I am looking for
			if (state != null && !stateStr.equals(state.toString()))
				continue;

			String eventStr = annotation.event();
			// I don't care about event (event == null), but method does
			// (Action.NULL)
			if (event == null && !eventStr.equals(Action.NULL))
				continue;
			// I do care about event (event != null), but method event is NOT
			// what I am looking for
			if (event != null && !eventStr.equals(event.toString()))
				continue;
			return method;
		}
		return null;
	}

	@SuppressWarnings("unchecked")
	public void processEvent(T target, E event) {
		S currentState = getCurrentState(target);
		// Try to find a exactly match for current state and event
		Method method = findAction(currentState, event);
		if (method == null)
			// Try to find a exactly match for current state and for any event
			method = findAction(currentState, null);
		if (method == null)
			// Try to find a exactly match for current event and for any state
			method = findAction(null, event);
		if (method == null)
			// Try to find an action for any state and any event
			method = findAction(null, null);
		if (method == null)
			// Gave up!
			return;
		Class<?> parameterTypes[] = method.getParameterTypes();
		Object[] parameters = new Object[parameterTypes.length];
		for (int i = 0; i < parameterTypes.length; i++) {
			if (parameterTypes[i].isInstance(target))
				parameters[i] = target;
			else if (parameterTypes[i].isInstance(event))
				parameters[i] = event;
			else if (parameterTypes[i].isInstance(currentState))
				parameters[i] = currentState;
			else if (parameterTypes[i].isInstance(owner))
				parameters[i] = owner;
		}

		try {
			Object result = method.invoke(owner, parameters);
			if (result != null && result.getClass().isInstance(currentState))
				setCurrentState(target, (S) result);

		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	private void setCurrentState(T target, S result) {
		Field[] fields = target.getClass().getFields();
		for (Field field : fields) {
			if (!field.getType().isAssignableFrom(State.class))
				continue;
			try {
				@SuppressWarnings("unchecked")
				State<S> state = (State<S>) field.get(target);
				state.setState(result);
			} catch (Exception e) {
				Log.w(TAG, e);
			}
		}
	}

	private S getCurrentState(T target) {
		Field[] fields = target.getClass().getFields();
		for (Field field : fields) {
			if (!field.getType().isAssignableFrom(State.class))
				continue;
			try {
				@SuppressWarnings("unchecked")
				State<S> state = (State<S>) field.get(target);
				return state.getState();
			} catch (Exception e) {
				Log.w(TAG, e);
			}
		}
		return null;
	}
}
